package com.declum.naduvar.locking.db;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.rmi.server.LogStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.concurrent.locks.Lock;

import org.apache.log4j.Logger;

public class LockDB {

	/**
	 * Logger object to log events
	 */
	protected Logger logger;

	/**
	 * Default Lease_Interval 60 Seconds
	 */
	protected int leaseInterval = 60000;

	/**
	 * This holds all Locks
	 */
	protected Map<String, LockStruct> lockdb;

	/**
	 * Whenever write happens, we make a lock on this Object
	 */
	protected Object writeLockObj;

	/**
	 * Key Generation
	 */
	private Random randomNumber;

	/**
	 * File to store the lock persistently
	 */
	protected String workingDirectory;
	private PrintWriter logfileWritter;

	/**
	 * Constructor that initialize the internal objects
	 */
	public LockDB() {
		// Initialize the Logger
		this.logger = Logger.getLogger(LockDB.class);

		// Initialize the lockdb (HashMap)
		this.lockdb = new HashMap<String, LockStruct>();

		// Initialize the Write Lock Object
		this.writeLockObj = new Object();

		// Initialize the Random Number
		this.randomNumber = new Random(System.currentTimeMillis());

		// Prepare the Lock Database.
		init();
	}

	private void init() {
		this.logger.info("initializing LockDB");
	}

	protected final LockStruct getLockStruct(String lockPath) {
		LockStruct lockStruct = null;
		Date currentTime = null;

		currentTime = new Date(System.currentTimeMillis());
		lockStruct = this.lockdb.get(lockPath);

		if (lockStruct != null) {
			this.logger.debug("lock found for " + lockPath);
			/**
			 * If the lock expired then we can remove the Lock on the PATH
			 */
			if (lockStruct.getModifiedTime().before(currentTime)) {
				synchronized (this.writeLockObj) {
					this.lockdb.remove(lockPath);
					logLockStruct("removelock", lockStruct);
				}
				this.logger.debug("lease expired from " + lockPath
						+ " and lock is removed");
				lockStruct = null;
			}
		}
		return lockStruct;
	}

	protected final LockStruct createlockStruct(String lockPath)
			throws LockException {
		LockStruct lockStruct = null;
		if (lockPath != null) {
			lockPath = lockPath.trim();
			lockStruct = new LockStruct();
			lockStruct.setLockPath(lockPath);
			long leaseEndTime = System.currentTimeMillis() + leaseInterval;
			Date currentTime = new Date(leaseEndTime);
			lockStruct.setCreatedTime(currentTime);
			lockStruct.setModifiedTime(currentTime);
			/**
			 * Generate the RandomKey for the Lock
			 */
			lockStruct.setLockKey((this.randomNumber.nextLong() + leaseEndTime)
					+ "");

			synchronized (this.writeLockObj) {
				if (!this.lockdb.containsKey(lockPath)) {
					this.lockdb.put(lockPath, lockStruct);
					logLockStruct("getlock", lockStruct);
				} else {
					throw new LockException("Lock already exists");
				}
			}
			this.logger.debug("lock acquired till "
					+ lockStruct.getCreatedTime() + " on " + lockPath);
		} else {
			throw new NullPointerException("LockPath cannot be NULL");
		}
		return lockStruct;
	}

	public void releaseLock(String path, String key) {
		LockStruct lockStruct = null;
		String chunk = null;
		String lockPath = "";

		path = path.trim();
		path = path.replace("\\", "/");
		StringTokenizer tokens = new StringTokenizer(path, "/");

		while (tokens.hasMoreTokens()) {
			chunk = tokens.nextToken();
			lockPath += "/" + chunk;
		}
		lockStruct = getLockStruct(lockPath);
		if (lockStruct == null) {
			/**
			 * Lock already gone
			 */
			this.logger
					.debug("lock not exists. release lock has not effect on the path "
							+ lockPath);
		} else {
			synchronized (this.writeLockObj) {
				/**
				 * Key needs to be checked for security risks
				 */
				this.lockdb.remove(lockPath);
				logLockStruct("removelock", lockStruct);
			}
			this.logger.debug("lock on " + lockPath + " released");
		}
	}

	public void renewLock(String path, String key) throws LockException {
		LockStruct lockStruct = null;
		String chunk = null;
		String lockPath = "";

		path = path.trim();
		path = path.replace("\\", "/");
		StringTokenizer tokens = new StringTokenizer(path, "/");

		while (tokens.hasMoreTokens()) {
			chunk = tokens.nextToken();
			lockPath += "/" + chunk;
		}
		lockStruct = getLockStruct(lockPath);
		if (lockStruct == null) {
			this.logger.debug("lock on " + lockPath
					+ " expired. renew lock has no effect");
			throw new LockException("The lock is expired");
		} else {
			/**
			 * Instead Global Object, think for some local Static objects or
			 * Values from LockStruct itself.
			 * 
			 * Keeping lock on WriteLock blocks all the threads
			 */
			synchronized (this.writeLockObj) {
				lockStruct
						.setModifiedTime(new Date(System.currentTimeMillis()));
				logLockStruct("modified", lockStruct);
			}
			this.logger.debug("lock on " + lockPath + " renewed");

		}
	}

	public LockStruct getLock(String path) throws LockException {
		LockStruct lockStruct = null;
		String chunk = null;
		String lockPath = "";

		path = path.trim();
		path = path.replace("\\", "/");
		StringTokenizer tokens = new StringTokenizer(path, "/");

		while (tokens.hasMoreTokens()) {
			chunk = tokens.nextToken();
			lockPath += "/" + chunk;

			lockStruct = getLockStruct(lockPath);

			if (lockStruct != null) {
				this.logger.debug("lock cannot be acquired on " + path
						+ " as the resource " + lockPath
						+ " already holds the lock");
				break;
			}
		}
		if (lockStruct == null) {
			lockStruct = createlockStruct(lockPath);
			this.logger.debug("lock is created for " + lockPath);
		} else {
			throw new LockException("The path '" + lockPath + "' is locked");
		}
		return lockStruct;
	}

	public int getLeaseInterval() {
		return this.leaseInterval;
	}

	public void setLeaseInterval(int leaseInterval) throws IOException {
		if (leaseInterval < 10000) {
			throw new IOException("Interval cannot be less than 10sec");
		}

		if (leaseInterval > 180000) {
			throw new IOException("Interval cannot be more than 3min");
		}
		this.leaseInterval = leaseInterval;
	}

	public void setWorkingDirectory(String workingDirectory) throws Exception {
		if (workingDirectory != null) {
			File directory = new File(workingDirectory);
			if (!directory.exists()) {
				this.logger
						.debug("directory does not exist. trying to create it");
				directory.mkdirs();
			}

			File logFile = new File(directory.getAbsolutePath()
					+ File.separator + "lockDB.log");
			if (logFile.exists()) {
				buildLockDB(logFile);
			}
			this.logfileWritter = new PrintWriter(logFile);
			this.workingDirectory = workingDirectory;
		} else {
			throw new IOException(
					"Has to be set before the starting the Server");
		}
	}

	private void buildLockDB(File logFile) throws IOException, ParseException,
			LockException {
		BufferedReader in = new BufferedReader(new FileReader(logFile));
		DateFormat format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
				DateFormat.SHORT);
		String line;
		LockStruct struct;
		do {
			line = in.readLine();
			if (line == null) {
				break;
			}

		} while (true);
	}

	public String getWorkingDirectory() {
		return this.workingDirectory;
	}

	private void logLockStruct(String action, LockStruct lock) {
		String line;
		DateFormat format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
				DateFormat.SHORT);
		line = action + "\t" + lock.getLockPath() + "\t" + lock.getLockKey()
				+ "\t" + format.format(lock.getCreatedTime()) + "\t"
				+ format.format(lock.getModifiedTime());
		this.logfileWritter.println(line);
	}
}
